/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package es.silenus.detecta.service.impl;

import es.silenus.detecta.dao.Page;
import es.silenus.detecta.dao.UserDAO;

import es.silenus.detecta.model.User;

import es.silenus.detecta.service.UserService;

import org.springframework.dao.DataAccessException;

import org.springframework.security.AccessDeniedException;
import org.springframework.security.Authentication;
import org.springframework.security.AuthenticationException;

import org.springframework.security.context.SecurityContextHolder;

import org.springframework.security.providers.AuthenticationProvider;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
import org.springframework.security.providers.encoding.PasswordEncoder;

import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UsernameNotFoundException;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;


/**
 * Default user service, to be used when DETECTA is a standalone application.
 *
 * @author <a href="mailto:malonso@silenus-consultoria.es">Mariano Alonso</a>
 *
 * @since 15-jun-2009 10:43:35
 */
public class DefaultUserService implements AuthenticationProvider, UserService {
	/**
	 * The user DAO.
	 */
	private UserDAO userDAO;

	/**
	 * The password encoder.
	 */
	private PasswordEncoder passwordEncoder;

	/**
	 * Sets the user DAO.
	 *
	 * @param userDAO the user DAO.
	 */
	public void setUserDAO(final UserDAO userDAO) {
		this.userDAO = userDAO;
	}

	/**
	 * Sets the password encoder.
	 *
	 * @param passwordEncoder the password encoder.
	 */
	public void setPasswordEncoder(final PasswordEncoder passwordEncoder) {
		this.passwordEncoder = passwordEncoder;
	}

	/**
	 * Performs authentication with the same contract as {@link
	 * org.springframework.security.AuthenticationManager#authenticate(org.springframework.security.Authentication)}.
	 *
	 * @param authentication the authentication request object.
	 *
	 * @return a fully authenticated object including credentials. May return <code>null</code> if the
	 * 				 <code>AuthenticationProvider</code> is unable to support authentication of the passed
	 * 				 <code>Authentication</code> object. In such a case, the next <code>AuthenticationProvider</code> that
	 * 				 supports the presented <code>Authentication</code> class will be tried.
	 *
	 * @throws AuthenticationException if authentication fails.
	 */
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
		if(!supports(authentication.getClass())) {
			return null;
		}

		UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken)authentication;

		String password = (String)token.getCredentials();
		String encodedPassword = passwordEncoder.encodePassword(password, null);


		User user = userDAO.findByLoginAndPassword(token.getName(), encodedPassword);

		if(user != null) {
			return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
		}

		return null;
	}

	/**
	 * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the indicated
	 * <Code>Authentication</code> object.<p>Returning <code>true</code> does not guarantee an
	 * <code>AuthenticationProvider</code> will be able to authenticate the presented instance of the
	 * <code>Authentication</code> class. It simply indicates it can support closer evaluation of it. An
	 * <code>AuthenticationProvider</code> can still return <code>null</code> from the {@link
	 * #authenticate(Authentication)} method to indicate another <code>AuthenticationProvider</code> should be tried.</p>
	 *  <p>Selection of an <code>AuthenticationProvider</code> capable of performing authentication is conducted at
	 * runtime the <code>ProviderManager</code>.</p>
	 *
	 * @param authentication DOCUMENT ME!
	 *
	 * @return <code>true</code> if the implementation can more closely evaluate the <code>Authentication</code> class
	 * 				 presented
	 */
	public boolean supports(final Class authentication) {
		return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
	}

	/**
	 * Create a new user with the supplied details.
	 *
	 * @param user the user.
	 */
	@Transactional(propagation = Propagation.REQUIRED)
	public void createUser(final UserDetails user) {
		User theUser = (User)user;

		userDAO.save(theUser);
	}

	/**
	 * Update the specified user.
	 *
	 * @param user the user.
	 */
	@Transactional(propagation = Propagation.REQUIRED)
	public void updateUser(final UserDetails user) {
		User theUser = (User)user;

		userDAO.update(theUser);
	}

	/**
	 * Remove the user with the given login name from the system.
	 *
	 * @param username the user name.
	 */
	@Transactional(propagation = Propagation.REQUIRED)
	public void deleteUser(final String username) {
		User user = userDAO.findByLogin(username);

		if(user != null) {
			userDAO.remove(user);
		}
	}

	/**
	 * Modify the current user's password. This should change the user's password in the persistent user repository
	 * (datbase, LDAP etc) and should also modify the current security context to contain the new password.
	 *
	 * @param oldPassword current password (for re-authentication if required)
	 * @param newPassword the password to change to
	 *
	 * @throws AccessDeniedException if access is denied.
	 */
	@Transactional(propagation = Propagation.REQUIRED)
	public void changePassword(final String oldPassword, final String newPassword) {
		Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();

		if(currentUser == null) {
			throw new AccessDeniedException(
				"Can't change password as no Authentication object found in context for current user.");
		}

		currentUser = authenticate(new UsernamePasswordAuthenticationToken(currentUser.getName(), oldPassword));

		User user = (User)currentUser.getPrincipal();

		user.setPassword(newPassword);
		userDAO.update(user);
	}

	/**
	 * Modify the current user's password. This should change the user's password in the persistent user repository
	 * (datbase, LDAP etc) and should also modify the current security context to contain the new password.
	 *
	 * @param userName the user name.
	 * @param oldPassword current password (for re-authentication if required)
	 * @param newPassword the password to change to
	 */
	@Transactional(propagation = Propagation.REQUIRED)
	public void changePassword(final String userName, String oldPassword, String newPassword) {
		User theUser = userDAO.findByLogin(userName);

		// Update local user password
		String encodedPassword = passwordEncoder.encodePassword(newPassword, null);

		theUser.setPassword(encodedPassword);
		userDAO.update(theUser);
	}

	/**
	 * Check if a user with the supplied login name exists in the system.
	 *
	 * @param username the user name.
	 *
	 * @return true if the user exists, false otherwise.
	 */
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public boolean userExists(final String username) {
		return userDAO.findByLogin(username) != null;
	}

	/**
	 * Locates a user by its internal identifier.
	 *
	 * @param id the internal identifier.
	 *
	 * @return the user.
	 */
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public User findUserById(Integer id) {
		return userDAO.findById(id);
	}

	/**
	 * Finds the objects that satisfy a search criteria expressed as an example object bean and a pagination object
	 * to limit and sort.
	 *
	 * @param sample the sample criteria.
	 * @param start the start index.
	 * @param limit the limit index.
	 * @param sortFields the sort fields.
	 * @param sortDirections the sort directions.
	 *
	 * @return the page.
	 */
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public Page<User> findByExample(
		Map<String, Object> sample, int start, int limit, String[] sortFields, String[] sortDirections) {
		return userDAO.findByExample(sample, start, limit, sortFields, sortDirections);
	}

	/**
	 * Locates the user based on the username. In the actual implementation, the search may possibly be case
	 * insensitive, or case insensitive depending on how the implementaion instance is configured. In this case, the
	 * <code>UserDetails</code> object that comes back may have a username that is of a different case than what was
	 * actually requested..
	 *
	 * @param username the username presented to the {@link
	 * 				org.springframework.security.providers.dao.DaoAuthenticationProvider}
	 *
	 * @return a fully populated user record (never <code>null</code>)
	 *
	 * @throws UsernameNotFoundException if the user could not be found or the user has no GrantedAuthority
	 * @throws DataAccessException if user could not be found for a repository-specific reason
	 */
	@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
	public User loadUserByUsername(final String username) throws UsernameNotFoundException, DataAccessException {
		User user = userDAO.findByLogin(username);

		if(user == null) {
			throw new UsernameNotFoundException("User " + username + " not found");
		}

		return user;
	}
}
